/* @flow */

import config from '../config'
import {warn} from './debug'
import {set} from '../observer/index'
import {unicodeRegExp} from './lang'
import {nativeWatch, hasSymbol} from './env'

import {
  ASSET_TYPES,
  LIFECYCLE_HOOKS
} from 'shared/constants'

import {
  extend,
  hasOwn,
  camelize,
  toRawType,
  capitalize,
  isBuiltInTag,
  isPlainObject
} from 'shared/util'

/**
 * 2019-05-15
 * 自定义合并策略：https://cn.vuejs.org/v2/api/#optionMergeStrategies
 */
/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies

/**
 * Options with restrictions
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

/**
 * 2019-05-15
 * 深度递归，合并数据
 */
/**
 * Helper that recursively merges two data objects together.
 */
function mergeData(to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * ----------------------------------------------------------------------------------
 * 2019-05-15
 * ？？1.data的合并都被置为一个function
 *    因为，通过函数返回数据对象，保证了每个组件实例都有一个唯一的数据副本，避免了组件间数据互相影响。
 *    后面讲到 Vue 的初始化的时候大家会看到，在初始化数据状态的时候，就是通过执行 strats.data 函数来获取数据并对其进行处理的
 * ？？2.为什么不在合并阶段就把数据合并好，而是要等到初始化的时候再合并数据
 *    在 Vue 的初始化的时候，会发现 inject 和 props 这两个选项的初始化是先于 data 选项的，
 *    这就保证了我们能够使用 props 初始化 data 中的数据
 */
/**
 * Data
 */
export function mergeDataOrFn(parentVal: any,
                              childVal: any,
                              vm?: Component): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn() {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn() {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

strats.data = function (parentVal: any,
                        childVal: any,
                        vm?: Component): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

/**
 * 2019-05-15
 * 生命周期函数合并，
 * 1.合并为数组；
 * 2.如果是Vue.extend创建的子类，合并之后的声明周期函数会包含父类的生命周期（感觉没有React的父子类处理的好，非手动调用super）
 * ----------------------------------------------------------------------------------
 */
/**
 * Hooks and props are merged as arrays.
 */
function mergeHook(parentVal: ?Array<Function>,
                   childVal: ?Function | ?Array<Function>): ?Array<Function> {
  /**
   * 2019-05-15
   * 调整代码格式
   */
  const res = childVal ?
    parentVal ?
      parentVal.concat(childVal)
      : Array.isArray(childVal) ? childVal : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

/**
 * 2019-05-15
 * 去重
 */
function dedupeHooks(hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * 2019-05-15
 * 内置组件
 */
/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */
function mergeAssets(parentVal: ?Object,
                     childVal: ?Object,
                     vm?: Component,
                     key: string): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * 2019-05-15
 * 被合并处理后的 watch 选项下的每个键值，有可能是一个数组，也有可能是一个函数
 */
/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (parentVal: ?Object,
                         childVal: ?Object,
                         vm?: Component,
                         key: string): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */
strats.props =
  strats.methods =
    strats.inject =
      strats.computed = function (parentVal: ?Object,
                                  childVal: ?Object,
                                  vm?: Component,
                                  key: string): ?Object {
        if (childVal && process.env.NODE_ENV !== 'production') {
          assertObjectType(key, childVal, vm)
        }
        if (!parentVal) return childVal
        const ret = Object.create(null)
        extend(ret, parentVal)
        if (childVal) extend(ret, childVal)
        return ret
      }
strats.provide = mergeDataOrFn

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * 2019-05-15
 * 判断options.components中Key是否符合规范
 * --------------------------------------------------------------------------------------------
 */
/**
 * Validate component names
 */
function checkComponents(options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}
export function validateComponentName(name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid（有效的） custom element name in html5 specification（规范）.'
    )
  }
  /**
   * 2019-05-15
   * isBuiltInTag：
   *    shared/util.js中定义
   *    是否为默认关键字名（slot、component）
   * isReservedTag：（reserved：保留）
   *    core/config.js中定义
   *    platforms/web/runtime/index中被重写，引用platforms/web/util/element.js
   *    是否为HTML、SVG规范中的标签
   */
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
/**
 * --------------------------------------------------------------------------------------------
 */

/**
 * 2019-05-15
 * 规范化props
 * 1.props: ["someData"]
 *   props: {
 *      someData:{
 *        type: null
 *      }
 *   }
 * 2.props: {
 *      someData1: Number,
 *      someData2: {
 *        type: String,
 *        default: ''
 *      }
 *  }
 *  props: {
 *      someData1: {
 *        type: Number
 *      },
 *      someData2: {
 *        type: String,
 *        default: ''
 *      }
 *  }
 */
/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 */
function normalizeProps(options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        /**
         * 2019-05-15
         * 转化为驼峰命名
         */
        name = camelize(val)
        res[name] = {type: null}
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : {type: val}
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

/**
 * 2019-05-15
 * 1.options.inject在vue 2.2.0 新增
 *    官方API：https://cn.vuejs.org/v2/api/#provide-inject
 * 2.示例（跨级传递属性）：
 *    // 子组件
 *    const ChildComponent = {
 *       template: '<div>child component</div>',
 *       created: function () {
 *         // 这里的 data 是父组件注入进来的
 *         console.log(this.data)
 *       },
 *       inject: ['data']
 *    }
 *    // 父组件
 *    var vm = new Vue({
 *       el: '#app',
 *       // 向子组件提供数据
 *       provide: {
 *         data: 'test provide'
 *       },
 *       components: {
 *         ChildComponent
 *       }
 *    });
 * 3.此种使用不恰当，会存在副作用：https://cn.vuejs.org/v2/guide/components-edge-cases.html#依赖注入
 *    1.逻辑耦合
 *    2.提供的属性是非响应式
 * 4.规范化inject
 *    let data1 = 'data1'
 *    inject: {
 *        data1,
 *        d2: 'data2',
 *        data3: { someProperty: 'someValue' }
 *    }
 *    -->
 *    inject: {
 *        'data1': { from: 'data1' },
 *        'd2': { from: 'data2' },
 *        'data3': { from: 'data3', someProperty: 'someValue' }
 *    }
 */
/**
 * Normalize all injections into Object-based format
 */
function normalizeInject(options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = {from: inject[i]}
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({from: key}, val)
        : {from: val}
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

/**
 * 2019-05-15
 * 1.directives 选项用来注册局部指令
 * 2.示例：
 *    <div id="app" v-test1 v-test2>{{test}}</div>
 *    var vm = new Vue({
 *        el: '#app',
 *        data: {
 *          test: 1
 *        },
 *        // 注册两个局部指令
 *        directives: {
 *          test1: {
 *            bind: function () {
 *              console.log('v-test1')
 *            }
 *          },
 *          test2: function () {
 *            console.log('v-test2')
 *          }
 *        }
 *    });
 */
/**
 * Normalize raw function directives into object format.
 */
function normalizeDirectives(options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = {bind: def, update: def}
      }
    }
  }
}

function assertObjectType(name: string, value: any, vm: ?Component) {
  if (!isPlainObject(value)) {
    warn(
      `Invalid value for option "${name}": expected an Object, ` +
      `but got ${toRawType(value)}.`,
      vm
    )
  }
}

/**
 * 2019-05-15
 * 合并两个选项对象为一个新的对象，这个函数在实例化和继承的时候都有用到
 */
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(parent: Object,
                             child: Object,
                             vm?: Component): Object {
  /**
   * 2019-05-15
   * 校验组件名，值得注意的是只在‘开发环境’判断
   */
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  /**
   * 2019-05-15
   * normalize（标准化）
   * 将简写的props:
   *    const ChildComponent = {
   *      props: ['someData']
   *    }
   * 规范化为：
   *    const ChildComponent = {
   *      props: {
   *        someData: {
   *          type: Number,
   *          default: 0
   *        }
   *      }
   *    }
   */
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    /**
     * 2019-05-15
     * 官方API：https://cn.vuejs.org/v2/api/#extends
     */
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  /**
   * 2019-05-15
   * 真正的合并阶段
   */
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField(key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }

  return options
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset(options: Object,
                             type: string,
                             id: string,
                             warnMissing?: boolean): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
